using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using Dimebrain.TweetSharp.Core.Extensions;

namespace visualtwimilar.Algorithms
{
  public static class Clusters
  {
    public static IDictionary<string, IList<double>> ScaleDown<T>(IDictionary<string, IDictionary<string, double>> prefs, double width, double height)
    {
      var users = prefs.Keys.ToList();
      const double rate = 0.01;

      double lasterror = 0d;
      var fakedist = new Dictionary<string, Dictionary<string, double>>();
      var realdist = new Dictionary<string, IDictionary<string, double>>();
      var loc = new Dictionary<string, IList<double>>();
      var grad = new Dictionary<string, List<double>>();

      foreach (var user1 in users)
      {
        if (!realdist.ContainsKey(user1))
          realdist[user1] = new Dictionary<string, double>();
        fakedist[user1] = new Dictionary<string, double>();
        foreach (var user2 in users)
        {
          if (!realdist[user1].ContainsKey(user2))
          {
           Stopwatch sw = new Stopwatch();
            sw.Start();
            realdist[user1][user2] = SimPearson(prefs, user1, user2);
            sw.Stop();
          }
          fakedist[user1][user2] = 0d;
        }
      }

      var rnd = new Random();
      foreach (var user in users)
      {
        loc[user] = new List<double>
                      {
                        rnd.Next((int)width),
                        rnd.Next((int)height)
                      };
      }

      for (int m = 0; m < 1000; m++)
      {
        foreach (var user1 in users)
        {
          foreach (var user2 in users)
          {
            double sum = 0d;
            for (int x = 0; x < loc[user1].Count; x++)
            {
              sum += Math.Pow(loc[user1][x] - loc[user2][x], 2);
            }
            fakedist[user1][user2] = Math.Sqrt(sum);
          }
        }
        foreach (var user in users)
        {
          grad[user] = new List<double> { 0, 0 };
        }

        var totalerror = 0d;
        foreach (var user1 in users)
        {
          foreach (var user2 in users)
          {
            if (user1 == user2) continue;
            var errorterm = (fakedist[user2][user1] - realdist[user2][user1]) / realdist[user2][user1];
            grad[user1][0] += ((loc[user1][0] - loc[user2][0]) / fakedist[user2][user1]) * errorterm;
            grad[user1][1] += ((loc[user1][1] - loc[user2][1]) / fakedist[user2][user1]) * errorterm;
            totalerror += Math.Abs(errorterm);
          }
        }
        
        if (lasterror != double.NaN | lasterror < totalerror)
          break;
        lasterror = totalerror;

        foreach (var k in users)
        {
          loc[k][0] -= rate * grad[k][0];
          loc[k][1] -= rate * grad[k][1];

        }
      }

      return loc;
    }

    public static double SimPearson(IDictionary<string, IDictionary<string, double>> prefs, string person1, string person2)
    {
      var si = new Dictionary<string, double>();
      var prefs1 = prefs[person1].Keys;
      var prefs2 = prefs[person2].Keys;

      prefs1.Filter(prefs2.Contains).Map(x => si[x] = 1);

      if (si.Count == 0) return 0;

      var n = si.Count;

      var sum1 = si.Keys.Map(x => prefs[person1][x])
        .Reduce<double, double>(0, (x, y) => x + y);
      var sum2 = si.Keys.Map(x => prefs[person2][x])
        .Reduce<double, double>(0, (x, y) => x + y);

      var sum1Sq = si.Keys.Map(x => prefs[person1][x])
        .Reduce<double, double>(0, (x, y) => Math.Pow(x, 2) + y);

      var sum2Sq = si.Keys.Map(x => prefs[person2][x])
        .Reduce<double, double>(0, (x, y) => Math.Pow(x, 2) + y);

      var pSum = si.Keys
        .Reduce<string, double>(0, (x, y) => prefs[person1][x] * prefs[person2][x] + y);

      var num = pSum - (sum1 * sum2 / n);
      var den = Math.Sqrt((sum1Sq - Math.Pow(sum1, 2) / n) * (sum2Sq - Math.Pow(sum2, 2) / n));

      if (den == 0)
        return 0;

      return num / den;
    }
  }
}